// This file is subject to a 1-clause BSD license.
// Its contents can be found in the enclosed LICENSE file.

// Package admin defines administrative bot commands.  This package is
// also used for the initial IRC login and channel joins.
package admin

import (
	"encoding/json"
	"log"
	"math"
	"os"
	"strconv"
	"strings"
	"syscall"
	"time"

	"notabug.org/mouz/bot/app"
	"notabug.org/mouz/bot/app/util"
	"notabug.org/mouz/bot/irc"
	"notabug.org/mouz/bot/irc/cmd"
	"notabug.org/mouz/bot/irc/proto"
	"notabug.org/mouz/bot/plugins"
)

// lastRestart defines the timestamp at which the bot was last restarted.
var lastRestart = time.Now()

func init() { plugins.Register(&plugin{}) }

type plugin struct {
	cmd *cmd.Set

	// This will store the bot's profile, but only as a subset of
	// the full interface. We only need access to some parts.
	profile interface {
		WhitelistAdd(string)
		WhitelistRemove(string)
		Whitelist() []string

		Nickname() string
		SetNickname(string)
		NickservPassword() string
		SetNickservPassword(string)

		Channels() []irc.Channel
	}
}

// Load initializes the module and loads any internal resources
// which may be required.
func (p *plugin) Load(prof irc.Profile) error {
	p.profile = prof
	p.cmd = cmd.New(
		prof.CommandPrefix(),
		prof.IsWhitelisted,
	)

	p.cmd.Bind(TextFloodName, false, p.cmdFlood).
		Add(TextFloodCount, true, cmd.RegAny)

	// Two aliases for the same command. Can be invoked through
	// !help or !<bot nickname>
	p.cmd.Bind(TextHelpName, false, p.cmdHelp)
	p.cmd.Bind(prof.Nickname(), false, p.cmdHelp)

	p.cmd.Bind(TextNickName, true, p.cmdNick).
		Add(TextNickNickName, true, cmd.RegAny).
		Add(TextNickPassName, false, cmd.RegAny)

	p.cmd.Bind(TextJoinName, true, p.cmdJoin).
		Add(TextJoinChannelName, true, cmd.RegChannel).
		Add(TextJoinKeyName, false, cmd.RegAny)

	p.cmd.Bind(TextPartName, true, p.cmdPart).
		Add(TextPartChannelName, true, cmd.RegChannel)

	p.cmd.Bind(TextNoopName, true, p.cmdNoop).
		Add(TextNoopChannelName, false, cmd.RegChannel)

	p.cmd.Bind(TextAuthListName, false, p.cmdAuthList)

	p.cmd.Bind(TextAuthorizeName, true, p.cmdAuthorize).
		Add(TextAuthorizeMaskName, true, cmd.RegAny)

	p.cmd.Bind(TextDeauthorizeName, true, p.cmdDeauthorize).
		Add(TextDeauthorizeMaskName, true, cmd.RegAny)

	p.cmd.Bind(TextReloadName, true, p.cmdReload)
	p.cmd.Bind(TextVersionName, false, p.cmdVersion)

	return nil
}

// Unload cleans the module up and unloads any internal resources.
func (p *plugin) Unload(prof irc.Profile) error {
	p.profile = nil
	return nil
}

// Dispatch sends the given, incoming IRC message to the plugin for
// processing as it sees fit.
func (p *plugin) Dispatch(w irc.ResponseWriter, r *irc.Request) {
	switch r.Type {
	case "375", "422": // received START_MOTD or NO_MOTD
		p.onFinalizeLogin(w, r)

	case "433":
		p.onNickInUse(w, r)

	case "PRIVMSG":
		p.cmd.Dispatch(w, r)
	}
}

// onFinalizeLogin is called to complete the login sequence. It is
// triggered when we receive either the STARTMOTD or NOMOTD messages.
// It identifies to nickserv and joins channels defined in the
// profile.
func (p *plugin) onFinalizeLogin(w irc.ResponseWriter, r *irc.Request) {

	if len(p.profile.NickservPassword()) > 0 {
		proto.PrivMsg(w, "nickserv", "IDENTIFY %s", p.profile.NickservPassword())
	}

	proto.Join(w, p.profile.Channels()...)

}

// onNickInUse signals that our nick is in use. If we can regain it, do so.
// Otherwise, change ours.
func (p *plugin) onNickInUse(w irc.ResponseWriter, r *irc.Request) {
	pr := p.profile

	pr.SetNickname(pr.Nickname() + "_")

	log.Println("[admin] Nick in use: changing nick to:", pr.Nickname())
	proto.Nick(w, pr.Nickname())
}

// cmdFlood
func (p *plugin) cmdFlood(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	j, _ := json.MarshalIndent(r, "", "  ")
	log.Println("[admin] cmdFlood() was called. Request:\n" + string(j))

	count := params.Uint(0)
	var i uint64
	for i = 0; i < count; i++ {
		proto.PrivMsg(w, r.Target, TextFloodDisplay, r.SenderName, i)
	}
}

// cmdHelp presents the user with a list of commands, also pointing them to
// a resource where the full bot help can be read.
func (p *plugin) cmdHelp(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {

	proto.PrivMsg(w, r.SenderName, TextHelpAll)

	for i := 1; i <= len(CommandsHelp); i++ {
		proto.PrivMsg(w, r.SenderName, "%s", CommandsHelp[i])
	}

	proto.PrivMsg(w, r.SenderName, TextHelpAdmins)
}

// cmdNick allows the bot to change its name.
func (p *plugin) cmdNick(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	p.profile.SetNickname(params.String(0))

	if params.Len() > 1 {
		proto.Nick(w, params.String(0), params.String(1))
		p.profile.SetNickservPassword(params.String(1))
	} else {
		proto.Nick(w, params.String(0))
	}
}

// cmdJoin makes the bot join a new channel.
func (p *plugin) cmdJoin(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	var channel irc.Channel
	channel.Name = params.String(0)

	if params.Len() > 1 {
		channel.Key = params.String(1)
	}

	proto.Join(w, channel)
}

// cmdPart makes the bot leave a given channel.
func (p *plugin) cmdPart(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	proto.Part(w, irc.Channel{
		Name: params.String(0),
	})
}

// cmdNoop makes the bot de-op itself.
func (p *plugin) cmdNoop(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	var channel_name string
	if params.Len() > 0 {
		channel_name = params.String(0)
	} else {
		if r.FromChannel() {
			channel_name = r.Target
		} else {
			// ugly?
			proto.PrivMsg(w, r.SenderName, cmd.TextMissingParameters, r.Data[1:])
			return
		}
	}
	proto.Mode(w, channel_name, "-o", p.profile.Nickname())
}

// cmdAuthList lists all whitelisted users.
func (p *plugin) cmdAuthList(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	list := p.profile.Whitelist()
	out := strings.Join(list, ", ")
	proto.PrivMsg(w, r.SenderName, TextAuthListDisplay, out)
}

// cmdAuthorize adds a new whitelisted user.
func (p *plugin) cmdAuthorize(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	p.profile.WhitelistAdd(params.String(0))
	proto.PrivMsg(w, r.SenderName, TextAuthorizeDisplay, params.String(0))
}

// cmdDeauthorize removes a user from the whitelist.
func (p *plugin) cmdDeauthorize(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	p.profile.WhitelistRemove(params.String(0))
	proto.PrivMsg(w, r.SenderName, TextDeauthorizeDisplay, params.String(0))
}

// cmdReload forces the bot to fork itself. This is achieved by
// sending SIGUSR1 to the current process.
func (p *plugin) cmdReload(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	syscall.Kill(os.Getpid(), syscall.SIGUSR1)
}

// cmdVersion prints version information.
func (p *plugin) cmdVersion(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	rev, _ := strconv.ParseInt(app.VersionRevision, 10, 64)
	stamp := time.Unix(rev, 0)

	hours := math.Abs(time.Since(lastRestart).Hours())
	days := hours / 24

	udays := math.Floor(days)
	uhours := 24 * (days - udays)
	uminutes := 60 * (uhours - math.Floor(uhours))
	useconds := 60 * (uminutes - math.Floor(uminutes))

	proto.PrivMsg(
		w, r.Target,
		TextVersionDisplay,
		r.SenderName,
		util.Bold(p.profile.Nickname()),
		util.Bold("%d.%d", app.VersionMajor, app.VersionMinor),
		stamp.Format(TextDateFormat),
		stamp.Format(TextTimeFormat),
		udays,
		uhours,
		uminutes,
		useconds,
	)
}
